/*
 * Copyright (c) 2010-2011, okazoh_tk. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  - Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *  - Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *  - Neither the name of the copyright holder nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <math.h>
#include "alkes/opengl/GLGraphics.h"
#include "alkes/opengl/GLVertexBuffer.h"
#include "alkes/opengl/GLVertexBufferVBO.h"
#include "alkes/opengl/GLImageBuffer.h"
#include "alkes/opengl/GLRenderTarget.h"
#include "alkes/opengl/GLImageBufferFactory.h"
#include "alkes/core/SharedLibrary.h"
#include "alkes/math/Math.h"
#include "alkes/math/Matrix.h"


#if !defined(ALKES_GLES)
#include "alkes/opengl/GLSoftwareMatrixStack.h"
inline alkes::IMatrixStack* _createGLSoftwareMatrixStack() { return new alkes::GLSoftwareMatrixStack(); }

#define CreateMatrixStack() \
    _createGLSoftwareMatrixStack()
#endif

#if !defined(ALKES_GLES2)
#include "alkes/opengl/GLMatrixStack.h"
inline alkes::IMatrixStack* _createGLMatrixStack() { return new alkes::GLMatrixStack(); }
#if !defined(CreateMatrixStack)
#define CreateMatrixStack() \
    _createGLMatrixStack()
#else
#undef CreateMatrixStack
#define CreateMatrixStack() \
    getOption(AL_OGLOPT_FEATURE_SHADER) ? _createGLSoftwareMatrixStack() : _createGLMatrixStack()
#endif
#endif

#define DestroyMatrixStack(stack)   delete stack


namespace alkes {


#if (ALKES_API_WIN32 == 1)
#define CODEC_LIB   "alkes-codec.dll"
#else
#define CODEC_LIB   "libalkes-codec.so"
#endif
#define DECODER_FACTORY_SYMBOL  "alkes_create_codec_factory"

    
#if !defined(ALKES_OGL_DISABLE_VBO)
#define INIT_VBUFFER(vertex_type)  \
    (false == getOption(AL_OGLOPT_FEATURE_VBO)) \
    ? 0                                         \
    : GLVertexBufferVBO::create(vertex_type)
#else
#define INIT_VBUFFER(vertex_type)  0
#endif

#if defined(ALKES_GLES)
static void gluPerspective(float fovy, float aspect, float znear, float zfar);
static void gluLookAt(
    float eyeX, float eyeY, float eyeZ,
    float centerX, float centerY, float centerZ,
    float upX, float upY, float upZ);
#endif


GLGraphics* GLGraphics::create(uint16_t width, uint16_t height, TargetBehavior behavior)
{
    return new GLGraphics(width, height, behavior);
}

void GLGraphics::destroy()
{
    delete this;
}

GLGraphics::GLGraphics(uint16_t width, uint16_t height, TargetBehavior behavior)
: width_(width)
, height_(height)
, behavior_(behavior)
, image_factory_(0)
, shared_(0)
, codec_factory_(0)
, gl_texture_(0)
, gl_array_buffer_(0)
, gl_element_array_buffer_(0)
{
    const char* str = (const char*)glGetString(GL_VENDOR);
    AL_NOTICE("GL_VENDOR: %s", str ? str : "");

    str = (const char*)glGetString(GL_RENDERER);
    AL_NOTICE("GL_RENDERER: %s", str ? str : "");

    // OpenGL => "2.0"
    // OpenGL ES => "OpenGL ES-XX 1.1" XX=CM,CL

    str = (const char*)glGetString(GL_VERSION);
    AL_NOTICE("GL_VERSION: %s", str ? str : "");
    gl_version_.set(atof(reinterpret_cast<const char*>(str)));

    //str = (const char*)glGetString(GL_EXTENSIONS);
    //AL_NOTICE("GL_EXTENSIONS: %s", str ? str : "");

    /*
    GL_ARB_texture_non_power_of_two
    */

#if !defined(ALKES_GLES)
    if (gl_version_ >= fixed_cast<Fixed16>(2.0f))
    {
        str = (const char*)glGetString(GL_SHADING_LANGUAGE_VERSION);
        AL_NOTICE("GL_SHADING_LANGUAGE_VERSION: %s", str ? str : "");
        shader_version_.set(atof(reinterpret_cast<const char*>(str)));
    }
    else
#endif
    {
        shader_version_ = fixed_cast<Fixed16>(0);
    }

    // VBO feature
    setOption(AL_OGLOPT_FEATURE_VBO, alkes_load_opengl_vbo());

    // FBO feature
    setOption(AL_OGLOPT_FEATURE_FBO, alkes_load_opengl_fbo());

    // shader feature
    setOption(AL_OGLOPT_FEATURE_SHADER, alkes_load_opengl_shader());

    state_.enable(AL_OGL_ST_CULL_FACE);
    state_.enable(AL_OGL_ST_SCISSOR_TEST);
    AL_OGL_INVOKE(glCullFace(GL_FRONT));
    AL_OGL_INVOKE(glFrontFace(GL_CCW));

    matrix_stack_ = CreateMatrixStack();
}

GLGraphics::~GLGraphics()
{
    if (shared_)
    {
        delete shared_;
    }

    if (matrix_stack_)
    {
        DestroyMatrixStack(matrix_stack_);
    }
}

void GLGraphics::setOption(OpenGLOption option, bool value)
{
    extension_flag_.setFlag(option, value);
}

bool GLGraphics::getOption(OpenGLOption option) const
{
    return extension_flag_[option];
}

uint16_t GLGraphics::getWidth() const
{
    return width_;
}

uint16_t GLGraphics::getHeight() const
{
    return height_;
}

int32_t GLGraphics::getGLYPos(int32_t window_y, int32_t rect_height) const
{
    return height_ - (window_y + rect_height);
}

int32_t GLGraphics::getAlkesYPos(int32_t window_y, int32_t rect_height) const
{
    return height_ - (window_y + rect_height);
}

void GLGraphics::resize(uint16_t width, uint16_t height)
{
    width_ = width;
    height_ = height;
}

void GLGraphics::flip()
{
}

GLState& GLGraphics::getStateContainer()
{
    return state_;
}

const GLState& GLGraphics::getStateContainer() const
{
    return state_;
}

DecoderFactory* GLGraphics::createDecoderFactory()
{
    if (!shared_)
    {
        shared_ = new SharedLibrary(CODEC_LIB);
        if (shared_)
            codec_factory_ = (DecoderFactory* (*)(ImageBufferFactory*))shared_->getFunction(DECODER_FACTORY_SYMBOL);
    }

    DecoderFactory* factory = 0;
    if (codec_factory_)
    {
        factory = codec_factory_(createImageBufferFactory());
    }

    return factory;
}

ImageBufferFactory* GLGraphics::createImageBufferFactory()
{
    if (!image_factory_)
    {
        image_factory_ = GLImageBufferFactory::create();
    }
    return image_factory_;
}

IRenderTarget2D* GLGraphics::createDefaultRenderTarget2D()
{
    return createDefaultRenderTarget3D();
}

IRenderTarget3D* GLGraphics::createDefaultRenderTarget3D()
{
    Rect rect = {0,0,getWidth(),getHeight()};
    return GLRenderTarget::create(rect, this, behavior_);
}

IVertexBuffer* GLGraphics::createVertexBuffer(uint32_t vertex_type)
{
    IVertexBuffer* vbuffer = INIT_VBUFFER(vertex_type);

    if (!vbuffer)
        vbuffer = GLVertexBuffer::create(vertex_type);

    return vbuffer;
}


void GLGraphics::beginDraw2D(GLRenderTarget* target)
{
    pushMatrix();
    current_target_ = target;

    state_.disable(AL_OGL_ST_DEPTH_TEST);
    matrix_stack_->setMode(AL_MM_PROJECTION);
    matrix_stack_->identity();
    matrix_stack_->ortho(0, getWidth(), getHeight(), 0, 10.f, -10.f);
    matrix_stack_->setMode(AL_MM_MODELVIEW);
    matrix_stack_->identity();
}

void GLGraphics::endDraw2D()
{
    popMatrix();
}

void GLGraphics::beginDraw3D(GLRenderTarget* target)
{
    pushMatrix();
    current_target_ = target;
    state_.enable(AL_OGL_ST_DEPTH_TEST);
    AL_OGL_INVOKE(glDepthFunc(GL_GEQUAL));
    matrix_stack_->setMode(AL_MM_PROJECTION);
    matrix_stack_->identity();
    matrix_stack_->setMode(AL_MM_MODELVIEW);
    matrix_stack_->identity();
}

void GLGraphics::endDraw3D()
{
    popMatrix();
}

void GLGraphics::clearBuffer(uint32_t buffer_flag, const Color& clear_color, const Rect* rect, float depth)
{
    GLbitfield field = 0;

    if ((buffer_flag & AL_BUFFER_DEPTH) && state_.isEnable(AL_OGL_ST_DEPTH_TEST))
    {
        AL_OGL_INVOKE(glClearDepth(depth));
        field = GL_DEPTH_BUFFER_BIT;
    }

    if (buffer_flag & AL_BUFFER_COLOR)
    {
        AL_OGL_INVOKE(glClearColor(clear_color.r/255.f, clear_color.g/255.f, clear_color.b/255.f, clear_color.a/255.f));
        field = GL_COLOR_BUFFER_BIT;
    }

    if (buffer_flag)
    {
        if (rect)
        {
            AL_OGL_INVOKE(glScissor(rect->x, getGLYPos(rect->y, rect->height), rect->width, rect->height));
        }
        AL_OGL_INVOKE(glClear(field));
    }
}

const static GLenum s_primitive_modes[] = {
    GL_POINTS,
    GL_LINES,
    GL_LINE_STRIP,
    GL_TRIANGLE_STRIP,
    GL_TRIANGLE_FAN,
    GL_TRIANGLES,
    GL_LINE_LOOP,

#if !defined(ALKES_OGL_DISABLE_QUAD_PRIM)
    GL_QUAD_STRIP,
    GL_QUADS,
#elif !defined(ALKES_OGL_DISABLE_POLYGON_PRIM)
    (GLenum)-1, // stub
    (GLenum)-1, // stub
#endif

#if !defined(ALKES_OGL_DISABLE_POLYGON_PRIM)
    GL_POLYGON,
#endif
};

void GLGraphics::setScissorRect(const Rect& rect)
{
    AL_OGL_INVOKE(glScissor(rect.x, getGLYPos(rect.y, rect.height), rect.width, rect.height));
    //AL_DBG("setScissorRect(%d,%d,%d,%d)", rect.x, rect.y, rect.width, rect.height);
}

const Rect GLGraphics::getScissorRect() const
{
    GLint params[4];
    AL_OGL_INVOKE(glGetIntegerv(GL_SCISSOR_BOX, params));

    Rect rect = {
        (int16_t)params[0],
        (int16_t)getAlkesYPos(params[1], params[3]),
        (int16_t)params[2],
        (int16_t)params[3]
    };

    return rect;
}

void GLGraphics::setViewport(const Viewport& viewport)
{
    viewport_ = viewport;

    AL_OGL_INVOKE(glViewport(viewport.x, getGLYPos(viewport.y, viewport.h), viewport.w, viewport.h));
}

const Viewport& GLGraphics::getViewport() const
{
    return viewport_;
}

void GLGraphics::setPerspective(float angle, float aspect, float znear, float zfar)
{
    matrix_stack_->setMode(AL_MM_PROJECTION);
    matrix_stack_->perspective(angle, aspect, znear, zfar);
    matrix_stack_->setMode(AL_MM_MODELVIEW);
}

void GLGraphics::setLookAt(const Vector3D& eye, const Vector3D& at, const Vector3D& up)
{
    matrix_stack_->setMode(AL_MM_PROJECTION);
    matrix_stack_->lookAt(eye, at, up);
    matrix_stack_->setMode(AL_MM_MODELVIEW);
}

void GLGraphics::setState(int state, bool enable)
{
    switch (state)
    {
    case AL_GS_ALPHA_BLEND:
        state_.set(AL_OGL_ST_BLEND, enable);
        break;
    case AL_GS_LINE_SMOOTH:
        state_.set(AL_OGL_ST_LINE_SMOOTH, enable);
        break;
    case AL_GS_POLYGON_SMOOTH:
        state_.set(AL_OGL_ST_POLYGON_SMOOTH, enable);
        break;
    case AL_GS_DEPTH_TEST:
        state_.set(AL_OGL_ST_DEPTH_TEST, enable);
        break;
    default:
        AL_NO_SUPPORT("%s state=%d", __FUNC_NAME__, state);
        break;
    }
}

bool GLGraphics::getState(int state)
{
    bool ret = false;
    switch (state)
    {
    case AL_GS_ALPHA_BLEND:
        ret = state_.isEnable(AL_OGL_ST_BLEND);
        break;
    case AL_GS_LINE_SMOOTH:
        ret = state_.isEnable(AL_OGL_ST_LINE_SMOOTH);
        break;
    case AL_GS_POLYGON_SMOOTH:
        ret = state_.isEnable(AL_OGL_ST_POLYGON_SMOOTH);
        break;
    case AL_GS_DEPTH_TEST:
        ret = state_.isEnable(AL_OGL_ST_DEPTH_TEST);
        break;
    default:
        AL_NO_SUPPORT("%s state=%d", __FUNC_NAME__, state);
        break;
    }
    return ret;
}

const static GLenum s_blend_table[] = {
    GL_ZERO,
    GL_ONE,
    GL_SRC_COLOR,
    GL_ONE_MINUS_SRC_COLOR,
    GL_DST_COLOR,
    GL_ONE_MINUS_DST_COLOR,
    GL_SRC_ALPHA,
    GL_ONE_MINUS_SRC_ALPHA,
    GL_DST_ALPHA,
    GL_ONE_MINUS_DST_ALPHA,
};

void GLGraphics::setAlphaBlendMode(AlphaBlendMode src_blend, AlphaBlendMode dst_blend)
{
    AL_OGL_INVOKE(glBlendFunc(s_blend_table[src_blend], s_blend_table[dst_blend]));
}

void GLGraphics::getAlphaBlendMode(AlphaBlendMode* src_blend, AlphaBlendMode* dst_blend) const
{
    // TODO: impl
    if (src_blend)
        *src_blend = AL_BLEND_ZERO;

    if (dst_blend)
        *dst_blend = AL_BLEND_ZERO;
}

bool GLGraphics::setTexture(Image* image, int stage)
{
    if (!image)
    {
        state_.disable(AL_OGL_ST_TEXTURE_2D);
        return true;
    }

    AL_PRE_COND(image, false);

    ImageBuffer* ib = image->getImageBuffer();
    if (!ib)
        return false;

    GLImageBuffer* texture = restrict_cast<GLImageBuffer*>(ib);

    if (!texture)
    {
        // FIXME: convert ib to GLImageBuffer?
        AL_ASSERT(false);
        return false;
    }

    GLuint texid;
    if (!texture->getTexture(texid))
    {
        AL_ASSERT(false);
        return false;
    }

    //AL_DBG("glBindTexture(texid=%d)", texid);
    state_.enable(AL_OGL_ST_TEXTURE_2D);
    AL_OGL_INVOKE(glBindTexture(GL_TEXTURE_2D, texid));

    texture_unit_.activate(stage);

    return true;
}

void GLGraphics::setTextureFilter(TexFilterValue min_filter, TexFilterValue mag_filter, int stage)
{
    texture_unit_.setTextureFilter(stage, min_filter, mag_filter);
}

void GLGraphics::getTextureFilter(TexFilterValue* min_filter, TexFilterValue* mag_filter, int stage)
{
    texture_unit_.getTextureFilter(stage, min_filter, mag_filter);
}

void GLGraphics::setTextureAddressMode(TexAddressMode u_mode, TexAddressMode v_mode, int stage)
{
    texture_unit_.setTextureAddressMode(stage, u_mode, v_mode);
}

void GLGraphics::getTextureAddressMode(TexAddressMode* u_mode, TexAddressMode* v_mode, int stage)
{
    texture_unit_.getTextureAddressMode(stage, u_mode, v_mode);
}

bool GLGraphics::drawPrimitive(IVertexBuffer* vbuffer, PrimitiveType type, uint32_t start, uint32_t count)
{
    if (sizeof(s_primitive_modes)/sizeof(s_primitive_modes[0]) <= type)
        return false;

    GLVertexBuffer* vb = restrict_cast<GLVertexBuffer*>(vbuffer);
    if (!vb)
        return false;

    if (!vb->setupVertex(this, state_, start, count))
        return false;

    AL_OGL_INVOKE(glDrawArrays(s_primitive_modes[type], start, count));
    //AL_DBG("glDrawArrays(%d)", mode);

    return true;
}

void GLGraphics::pushMatrix()
{
    matrix_stack_->pushMatrix();
}

void GLGraphics::popMatrix()
{
    matrix_stack_->popMatrix();
}

void GLGraphics::translate(float x, float y)
{
    matrix_stack_->translate(x, y);
}

void GLGraphics::scale(float xscale, float yscale, const Vector2D& origin)
{
    matrix_stack_->scale(xscale, yscale, origin);
}

void GLGraphics::rotate(float angle, const Vector2D& origin)
{
    matrix_stack_->rotate(angle, origin);
}

void GLGraphics::skewX(float angle)
{
    matrix_stack_->skewX(angle);
}

void GLGraphics::skewY(float angle)
{
    matrix_stack_->skewY(angle);
}

void GLGraphics::translate(float x, float y, float z)
{
    matrix_stack_->translate(x, y, z);
}

void GLGraphics::scale(float xscale, float yscale, float zscale)
{
    matrix_stack_->translate(xscale, yscale, zscale);
}

void GLGraphics::rotate(float angle, float x, float y, float z)
{
    matrix_stack_->rotate(angle, x, y, z);
}

bool GLGraphics::supportsShader() const
{
    return getOption(AL_OGLOPT_FEATURE_SHADER);
}

bool GLGraphics::isActiveShader() const
{
    return supportsShader();
}

bool GLGraphics::supportsVBO() const
{
    return getOption(AL_OGLOPT_FEATURE_VBO);
}

void GLGraphics::bindTexture(GLenum target, GLuint texture)
{
    if (gl_texture_ != texture)
    {
        AL_OGL_INVOKE(glBindTexture(target, texture));
        gl_texture_ = texture;
    }
}

void GLGraphics::genBuffers(GLsizei num, GLuint* buffers)
{
    AL_OGL_INVOKE(glGenBuffers(num, buffers));

    GLsizei i;
    for (i = 0; i < num; ++i)
    {
        if (gl_array_buffer_ == buffers[i])
            gl_array_buffer_ = 0;

        if (gl_element_array_buffer_ == buffers[i])
            gl_element_array_buffer_ = 0;
    }
}

void GLGraphics::bindBuffer(GLenum target, GLuint buffer, bool& modified)
{
    GLuint* pbuffer = NULL;
    if (GL_ARRAY_BUFFER == target)
    {
        pbuffer = &gl_array_buffer_;
    }
    else if (GL_ELEMENT_ARRAY_BUFFER == target)
    {
        pbuffer = &gl_element_array_buffer_;
    }
    else
        return ;

    if (*pbuffer != buffer)
    {
        AL_OGL_INVOKE(glBindBuffer(target, buffer));
        *pbuffer = buffer;
        modified = true;
    }
    else
    {
        modified = false;
    }
}

void GLGraphics::bufferData(GLenum target, GLsizeiptr size, const GLvoid* data, GLenum usage)
{
    AL_OGL_INVOKE(glBufferData(target, size, data, usage));
}

}
